# -*- coding: utf-8 -*-
# @Time: 2021/9/7 22:32
# @Author: yh
# @Remark: 执行任务
import logging
import asyncio
import time

from asgiref.sync import sync_to_async
from mxsoftpy.exception import DataError

from db.common.VideoManage.VideoQuality.conf import TestItemsDB
from db.common.VideoManage.VideoQuality.inspection_task import InspectionTaskDB
from db.common.VideoManage.VideoQuality.task import TaskDB
from db.common.VideoManage.VideoQuality.video import VideoDB
from model.common.VideoManage.VideoQuality.inspection_task import InspectionTaskEndModel, InspectionTaskBeginModel
from model.common.VideoManage.VideoQuality.video import VideoTestUpdateModel
from server.common.VideoManage.VideoQuality.video_analyze import VideoAnalyze
from server.common.VideoManage.VideoQuality.video_type import VideoUrl
from utils.classes.custom_exception import ConnectionURLError, MethodExecError
from utils.conf.mxconfig import MxConfig


class TaskServer(object):

    def __init__(self, task_code: str):
        self.task_code = task_code
        self.test_items = TestItemsDB('TestItems').use_keys([])  # 所有检测项目
        self.__video_test_items = dict()  # 存放每个视频的检测项目，方便检测失败后生成数据

    def __check_test_item(self, item_code: str) -> dict:
        """
        判断检测项目是否合法并返回检测项目信息
        :param item_code: 检测项目code
        :return: 该检测项目详情
        """
        # 判断检测项目code是否合法
        if self.test_items.get(item_code) and self.test_items[item_code]['state'] == 1:
            return self.test_items[item_code]
        else:
            raise DataError('检测项目无效或不在用，项目code：%s' % item_code)

    def __generation_failed_data(self, video_code: str) -> list:
        """
        生成失败数据（当连接不上时）
        :param video_code: 当前检测的视频code
        :return: 失败数据
        """
        res = list()
        for k, v in self.__video_test_items[video_code].items():
            res.append([('task_code', self.task_code), ('video_code', video_code),
                        ('test_code', v['test_code']), ('test_value', -99999), ('test_state', 2),
                        ('test_time', int(time.time()))])

        return res

    async def __exec_video(self, video_code):
        """
        异步执行多个video的检测
        """
        test_num = 0  # 检测数量
        normal_num = 0  # 正常数量
        fault_num = 0  # 故障数量
        failed_num = 0  # 失败数量
        video_res = list()  # 存放视频检测数据
        running_error = list()  # 本视频所有检测故障的项目code
        running_normal = list()  # 本视频所有检测成功的项目code
        running_fail = list()  # 本视频所有检测失败的项目code

        logging.info('开始检测视频，任务code：%s， 视频code：%s' % (self.task_code, video_code))

        video_test_items = TaskDB(self.task_code).video_test_items()  # 所有检测项目
        self.__video_test_items[video_code] = video_test_items
        url = VideoUrl().url('HIKVISION', code=video_code)
        print(url)
        video_analyze = VideoAnalyze()
        # TODO 超时的处理
        try:
            handle = await sync_to_async(video_analyze.fetch_url)(url)  # 异步获取一帧图片
        except ConnectionURLError:
            logging.error('视频url连接失败，检测任务code：%s，视频code：%s，视频url：%s' % (self.task_code, video_code, url))
            raise DataError('检测任务失败')

        # 使用gather并发运行异步
        test_res_list = await asyncio.gather(
            *[self.__exec_test(value['test_code'], video_analyze, handle, url) for value in
              video_test_items.values()], return_exceptions=True)

        test_time = int(time.time())

        for index, key in enumerate(video_test_items):

            test_num += 1

            if not isinstance(test_res_list[index], float or int):
                failed_num += 1
                running_error.append(video_test_items[key]['test_code'])
                video_res.append([('task_code', self.task_code), ('video_code', video_code),
                                  ('test_code', video_test_items[key]['test_code']), ('test_value', -999),
                                  ('test_state', 2), ('test_time', test_time)])
                logging.error('检测任务code：%s，检测视频code：%s， 检测项目code：%s发生错误：' % (
                    self.task_code, video_code, video_test_items[key]['test_code']) + str(test_res_list[index]))
                continue

            else:
                item_info = self.test_items[video_test_items[key]['test_code']]
                expression = item_info['expression'].replace('value', str(test_res_list[index])).replace(
                    'confidence', str(video_test_items[key]['confidence']))
                logging.debug('检测任务code：%s，检测视频code：%s，检测项目code：%s，判断评测值是否合规，执行判断表达式：%s' % (
                    self.task_code, video_code, video_test_items[key]['test_code'], expression))
                if eval(expression):
                    logging.debug('检测任务code：%s，检测视频code：%s，检测项目code：%s，不符合设定值，检测结果：故障' % (
                        self.task_code, video_code, video_test_items[key]['test_code']))
                    fault_num += 1
                    running_error.append(video_test_items[key]['test_code'])
                    video_res.append([('task_code', self.task_code), ('video_code', video_code),
                                      ('test_code', video_test_items[key]['test_code']),
                                      ('test_value', test_res_list[index]), ('test_state', 3),
                                      ('test_time', test_time)])
                else:
                    logging.debug('检测任务code：%s，检测视频code：%s，检测项目code：%s，符合设定值，检测结果：正常' % (
                        self.task_code, video_code, video_test_items[key]['test_code']))
                    normal_num += 1
                    running_normal.append(video_test_items[key]['test_code'])
                    video_res.append([('task_code', self.task_code), ('video_code', video_code),
                                      ('test_code', video_test_items[key]['test_code']),
                                      ('test_value', round(test_res_list[index], 5)), ('test_state', 1),
                                      ('test_time', test_time)])

        # 评测保存图片 eg：30_2_1636787345.bmp
        path = '%s%s_%s_%s.bmp' % (MxConfig.TMP_DIR, video_code, self.task_code, str(test_time))
        video_analyze.exec_fun('py_saveFrameWithText', handle, '0', path)
        video_analyze.pop_handle(handle)

        return test_num, normal_num, fault_num, failed_num, video_res, running_error, running_normal, running_fail

    async def __exec_test(self, test_code: str, instance: "VideoAnalyze", handle: object, url: str):
        """
        对每个video的检测项目异步执行检测
        :param test_code: 检测项目code
        :param instance: 检测类的实例，详情见VideoAnalyze类
        :param handle: 检测实例打开url后返回的句柄
        :param url: video的url（某些检测方法不使用句柄，需要传入url）
        """
        item_info = self.__check_test_item(test_code)

        if item_info['func_args'] == 'handle':
            res = await sync_to_async(instance.exec_fun)(item_info['func_name'], handle)
        elif item_info['func_args'] == 'url':
            res = await sync_to_async(instance.exec_fun)(item_info['func_name'], url)

        else:
            logging.error('执行检测函数：%s时发现函数无法理解的参数类型：%s，当前支持的类型：handle、url' %
                          (item_info['func_name'], item_info['func_args']))
            raise MethodExecError(item_info['func_name'], '无法理解的参数类型：%s，当前支持的类型：handle、url'
                                  % item_info['func_args'])

        logging.info('检测项目：%s，获取评检测值成功：%s' % (item_info['name'], res))
        return res

    async def start(self):
        """
        开始执行此条任务
        """
        device_num = 0  # 设备数量
        test_num = 0  # 检测数量
        normal_num = 0  # 正常数量
        fault_num = 0  # 故障数量
        failed_num = 0  # 失败数量
        video_data = dict()  # 存放所有的视频检测数据
        running_dict = dict()  # 存放所有摄像头的正常和错误的检测项目

        InspectionTaskDB().update(self.task_code, **InspectionTaskBeginModel().dict())
        task_db = TaskDB(self.task_code)

        logging.info('开始执行任务，任务code：%s' % self.task_code)
        videos = task_db.videos()

        # 多个video并发执行
        video_res_list = await asyncio.gather(*[self.__exec_video(video_code) for video_code in videos],
                                              return_exceptions=True)

        for index, i in enumerate(video_res_list):
            device_num += 1
            video_items = VideoDB().retrieve(videos[index])
            running_dict[videos[index]] = dict()
            running_error_video = video_items.get('running_error').split(';') if video_items.get(
                'running_error') else []
            running_normal_video = video_items.get('running_normal').split(';') if video_items.get(
                'running_normal') else []
            running_fail_video = video_items.get('running_fail').split(';') if video_items.get('running_fail') else []
            e_diff = set(running_error_video).difference(set(self.test_items))  # 停用或不存在的错误检测项目code
            n_diff = set(running_normal_video).difference(set(self.test_items))  # 停用或不存在的正常检测项目code
            f_diff = set(running_fail_video).difference(set(self.test_items))  # 停用或不存在的检测失败检测项目code
            if e_diff:
                logging.error('校验视频code：%s的running_error列表时，发现停用或不存在的检测项目code：%s，将从列表中删除这些code'
                              % (videos[index], str(e_diff)))
                # 移除所有错误的值
                for j in e_diff:
                    running_error_video.remove(j)

            if n_diff:
                logging.error('校验视频code：%s的running_normal_video列表时，发现停用或不存在的检测项目code：%s，将从列表中删除这些code'
                              % (videos[index], str(n_diff)))
                # 移除所有错误的值
                for j in n_diff:
                    running_normal_video.remove(j)

            if f_diff:
                logging.error('校验视频code：%s的running_normal_video列表时，发现停用或不存在的检测项目code：%s，将从列表中删除这些code'
                              % (videos[index], str(f_diff)))
                # 移除所有错误的值
                for j in f_diff:
                    running_fail_video.remove(j)

            # 正常检测项目
            if isinstance(i, tuple):
                test_num += i[0]
                normal_num += i[1]
                fault_num += i[2]
                failed_num += i[3]
                video_data[videos[index]] = i[4]
                running_error_video = list(set(running_error_video) - set(i[6]) - set(i[7]))  # 去掉错误检测项目中本次检查正常的项目
                running_error_video.extend(i[5])
                running_dict[videos[index]]['error'] = running_error_video
                running_normal_video = list(set(running_normal_video) - set(i[5]) - set(i[7]))  # 去掉正常检测项目中本次检查错误的项目
                running_normal_video.extend(i[6])
                running_dict[videos[index]]['normal'] = running_normal_video
                running_fail_video = list(set(running_fail_video) - set(i[5]) - set(i[6]))
                running_fail_video.extend(i[7])
                running_dict[videos[index]]['fail'] = running_fail_video
                # running_normal[videos[index]] = running_normal_video

            # 连接url失败或其他错误
            else:
                try:
                    failed_num += len(self.__video_test_items[videos[index]])
                    test_num += len(self.__video_test_items[videos[index]])
                except DataError:
                    test_num += len(self.test_items)
                    failed_num += len(self.test_items)
                    logging.error("数据异常")
                video_data[videos[index]] = self.__generation_failed_data(videos[index])
                running_dict[videos[index]]['fail'] = [i for i in self.test_items]
                running_dict[videos[index]]['normal'] = []
                running_dict[videos[index]]['error'] = []

        # 插入记录到巡检记录表
        task_db.create_record(video_data)

        # 更新任务检测结果
        InspectionTaskDB().update(self.task_code,
                                  **InspectionTaskEndModel(device_num=device_num, test_num=test_num,
                                                           normal_num=normal_num, fault_num=fault_num,
                                                           failed_num=failed_num).dict())

        # 更新设备检测信息
        for k, v in running_dict.items():
            error = list(set(v.get('error')))  # 相同检测项目去重
            error.sort()
            normal = list(set(v.get('normal')))
            normal.sort()
            fail = list(set(v.get('fail')))
            fail.sort()

            param_dict = {
                'running_error': ';'.join(error),
                'running_normal': ';'.join(normal),
                'running_fail': ';'.join(fail),
            }
            VideoDB().task_update(k, **VideoTestUpdateModel(**param_dict).dict())
